/*
 * Author: Yuan Li(yuanli1), Chen Yang(chenyang)
 * Last Modified Day:04/27/2011
 * Summary: This is a simple proxy program which will only handle GET method 
 * and use small cache to speedup the response time. So we directly discard 
 * other Http method except for GET. And we will modify the oringal Http header
 * from the client to cancel the persisent request. In our cache stage, we will
 * not exam whether the contents stored in our cache has been changed in the 
 * server, instead, we simply give the client cached contents if the response
 * of the current request has been stored in our cache. Finally, when we decide
 * to evict a cache item to save a new response if the cache is full, we will
 * pick up the least recently used one but the new response must not exceed 
 * 100KB.
 */


#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "csapp.h"


/* debug macro */
//#define DEBUG
#ifdef DEBUG
# define dbg_printf(...) printf(__VA_ARGS__)
#else
# define dbg_printf(...) 
#endif

//#define DEBUG_CACHE
#ifdef DEBUG_CACHE
# define dbgC_printf(...) printf(__VA_ARGS__)
#else
# define dbgC_printf(...) 
#endif
/*------------------------------------------*/



/* my macro is defined below */
#define MAX_CACHE_SIZE 	1048576   // maximum cache size
#define MAX_OBJECT_SIZE 102400    // maximum object size
/*----------------------------------------*/



/* structure */
typedef struct{
	int serverPort;
	char method[MAXLINE],url[MAXLINE];
	char version[MAXLINE],hostname[MAXLINE],path[MAXLINE];
} requestHeader;

struct cacheSet{
        char hostname[MAXLINE];    // hostname
        char path[MAXLINE];        // path
        struct cacheSet* next;     // pointer to next cache set
	char* buffer;              // buffer pointer
        int time;    	 	   // time stamp
	int len;      		   // buffer size
};
/*-------------------------------------------------*/


/* our help functions are declared below        */
void *doGet(void*);
struct cacheSet *cacheSearch(char *hostname, char *path);
struct cacheSet *cacheFindLRU(int len);
int cacheAdd(char *hostname, char *path, char *buf, int len);
/*----------------------------------------------*/


/* global variables*/
int globalTime = 0;       //record as time stamp
struct cacheSet* cache;   //pointer to the beginning of the cache        
int readCount = 0;        //reader number
sem_t mutex_time;         //mutex for global time change
sem_t mutex, w;           //mutex for reading and writing cache
sem_t mutexHostName;      //mutex for gethostname
/*-----------------------------------------------------------*/



int main (int argc, char *argv [])
{
	pthread_t tid;
	struct sockaddr_in address;
	int listenfd;
	int *connection;
	int listenPort;
	socklen_t addressLen = sizeof(struct sockaddr_in);    

        cache = malloc(sizeof(struct cacheSet));
        if(cache == NULL){
		fprintf(stderr, "fail to malloc cache.\n");
		exit(1);
	}
       
	/* initilize cache */
	strcpy(cache->hostname, "");
        strcpy(cache->path, "");
        cache->next = NULL;
        cache->buffer = NULL;
        cache->time = 0;
        cache->len = 0;

        /* initilize semaphore */
        sem_init(&mutex_time,0,1);
        sem_init(&mutex,0,1);
        sem_init(&w,0,1);
	sem_init(&mutexHostName,0,1);

	/* exam the portnumber */
	if (argc != 2){
		fprintf(stderr, "No portnumber!\n");
		exit(1);
	}

	/* ignore the SIGPIPE*/
	Signal(SIGPIPE,SIG_IGN);
	
	/* main proxy thread will always listen on the listenPort*/
	listenPort = atoi(argv[1]);
	if ((listenfd = open_listenfd(listenPort)) < 0){
		fprintf(stderr,"fail to open listenfd!\n");
		fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
		exit(-1);
	}
	/* main proxy thread keep accepting a connect then sprawn a
	 * new thread to serve it
         */
	while(1){
    		/* apply a space from heap for a new thread*/
		if ((connection = (int *) malloc(sizeof(int)))== NULL){
			fprintf(stderr,"fail to get space from heap!!\n");
			fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
			/* retry instead of exiting*/
			continue;
		}	

    		/* accept a connect request from the client*/
		if ((*connection = accept(listenfd,(SA *)&address,
&addressLen)) < 0){
			fprintf(stderr,
"fail to accept a connection request!\n");
			fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
			continue;
		}
		
		/* sprawn a thread to serve the above request*/
		if ((pthread_create(&tid,NULL,doGet,connection)) != 0){
			fprintf(stderr,"fail to create a thread!!\n");
			fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
			continue;
		}
		
	}		
	
  	printf("the main thread should never get here!!\n");
	return 0;
}

/*
 * parseHeader: peer thread function to parse a requestHeader
 * @param (requestHeader* header): a pointer refering to the header
 * structure which is local variable defined in the doGet function
 * @param (rio_t* clientRio): a pointer refering to the rio_t variable
 * which is local variable defined in the doGet function
 * @return (int) success return 0, failure return -1
 */

int parseHeader(requestHeader* header,rio_t* clientRio)
{	
	char *ptr;
	int i=0,j=0;
	int length;
	char buf[MAXLINE];
	char temp[MAXLINE];
	char port[MAXLINE]="80"; 

	/*  whether the first line contains hostname */
	if (!strncasecmp(header->url,"http://",7)){
		/* surpass "http://"*/
		strcpy(header->url,(header->url+7));
		/* whether path is the root directory */
		if (!(ptr = strchr(header->url,'/')))
			strcpy(header->path,"/");
		/* get the path*/
		else{
			strcpy(header->path,ptr);		
		}
		
		/* get the hostname */
		length = strlen(header->url);
		for(i=0;i<length;i++){

			if(header->url[i] == '/')
				break;
			if(header->url[i] == ':')
				break;

			header->hostname[i] = header->url[i];
		}
		header->hostname[i] = '\0';
		
		/* get port number */
		if (header->url[i] == ':'){
			i++;
			length = strlen(header->url+i);
			for(j=0;length;j++,i++){

				if(header->url[i] == '/')
					break;
	
				port[j] = header->url[i];
			}
			port[j] = '\0';
		}
	}				
	
	else{
		/* store the path */
		strcpy(header->path,header->url);
		/* read the second line ?? will it read too many line?*/
		while(1){
	
			if ((rio_readlineb(clientRio,buf,MAXLINE)) <0){
				fprintf(stderr,
"fail to read a line from client stream!\n");
				fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
				return -1;
			}
		
			if (!strncasecmp(buf,"Host:",5))
				break;
		}
		
		sscanf(buf,"%s %s",temp,header->hostname);
		if ((ptr = strchr(header->hostname,':'))){
			*ptr = ' ';
			sscanf(header->hostname,"%s %s",temp,port);
			strcpy(header->hostname,temp);
		}
		else
			return -1;
		
	}
	
	header->serverPort = atoi(port);
	
 	return 0;	
}


/*
 * doGet: peer thread function to serve the single client
 * @param (int *paraCon): the connection file descriptor 
 * which create by main thread
 */
void *doGet(void *vargp)
{
	rio_t clientRio,serverRio;
	int con = *(int *)vargp;
	pthread_t tid;
	int length;
	char buf[MAXLINE], recBuf[MAX_OBJECT_SIZE];
	requestHeader header;
	int con2Server;	
	
	struct cacheSet* set;
        int lenSum = 0;

	/* free the heap space first avoid mem leaks when this thread die*/
	free(vargp);

	/* get the current thread tid
	 * I think we do not need to worry about pthread_self() failure
	 * even it will fail, we can still detect when detaching thread
	*/
	tid = pthread_self(); 

	/* detach the thread because we do not want main to reap it*/
	if ((pthread_detach(tid)) != 0){
		fprintf(stderr,"fail to detach thread:%d\n",(int) tid); 
		fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
		close(con);
		pthread_exit(NULL);
   	}

   	/* read the first line of header in the request*/
	rio_readinitb(&clientRio,con);

	if (rio_readlineb(&clientRio,buf,MAXLINE)<0){
		fprintf(stderr,
"fail to read the first line from the request!\n");
		fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
		close(con);
		pthread_exit(NULL);
	}
	
	/* parse the request to get method,url,version,serverPort,hostname*/
	sscanf(buf,"%s %s %s",header.method,header.url,header.version);
	dbg_printf("method=%s,url=%s,version=%s\n",
header.method,header.url,header.version);
	/* we only surpport GET method*/
	if (strcmp(header.method,"GET")){
		fprintf(stderr,"we cannot forward %s method!!\n",
header.method);
		close(con);
		pthread_exit(NULL);
	}	
	if (parseHeader(&header,&clientRio)<0){
		fprintf(stderr,"fail to parse the header)!\n");
		fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
		close(con);
		pthread_exit(NULL);
	}
	
	
	dbg_printf("serverPort=%d,hostname=%s\n",
header.serverPort,header.hostname);
	dbg_printf("parsed the request\n");
	
	/* initilize variable for cache search */
	strcpy(recBuf,"");
	lenSum = 0;

        /* readers mutex, support sharing readers */
        P(&mutex);
        readCount++;
        if(readCount == 1) P(&w);
        V(&mutex);

        /* do the reading */
	set = cacheSearch(header.hostname, header.path);
        if(set != NULL){
		lenSum = set->len;
  		memcpy(recBuf,set->buffer,lenSum);
        }

        P(&mutex);
        readCount--;
        if(readCount == 0) V(&w);
        V(&mutex);

        if(lenSum != 0){
		if((rio_writen(con,recBuf,lenSum)) != lenSum){
			fprintf(stderr, "fail to write to client.\n");
                	fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
                	close(con);
                	pthread_exit(NULL);
         	}
                close(con);
   		return NULL;
	}	

	/* forward http request to the server*/
        /* establish a connection to the server, check the return value in case 
	 * user inputs some wrong domain names
	 */
	if ((con2Server = open_clientfd(header.hostname,header.serverPort))<0){
		fprintf(stderr,"cannot open the clientfd\n");
		fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
		close(con);
		pthread_exit(NULL);
	}

	/* write the first two lines of header contents*/
	sprintf(buf,"%s %s HTTP/1.0\r\nHost: %s\r\n",
header.method,header.path,header.hostname);
	if ((rio_writen(con2Server,buf,strlen(buf))) != strlen(buf)){
		fprintf(stderr,"fail to write to the server!!");
		fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
		close(con);
		close(con2Server);
		pthread_exit(NULL);
	}
	
	/* write other lines of header contents*/
	while(1){
		/* read a line into the local buffer */
		if ((length = rio_readlineb(&clientRio,buf,MAXLINE))<0){
			fprintf(stderr,
"fail to read a line from the request!");
			fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
			close(con);
			close(con2Server);
			pthread_exit(NULL);
		}

		dbg_printf("read from client: %s\n",buf);
		
		/* we have write the second line, so this time just skip it*/
		if (!strncasecmp(buf,"Host",4))
			continue;
		
		/* we do not want keep the connect alive, so skip it too*/
		if (!strncasecmp(buf,"Keep-Alive",10))
			continue;

		/* we want server close the connection after send back 
		 * all the contents which the client requests
		 */
		if (!strncasecmp(buf,"Connection",10)){
			strcpy(buf,"Connection: close\r\n");
			length = strlen(buf);
		}

		if (!strncasecmp(buf,"Proxy-Connection",16)){
			strcpy(buf,"Proxy-Connection: close\r\n");
			length = strlen(buf);
		}
	
		/* send the contents of buffer to the server */
		if ((rio_writen(con2Server,buf,length)) != length){
			fprintf(stderr, "fail to write to the server!\n");
			fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
			close(con);
			close(con2Server);
			pthread_exit(NULL);
		}	

		dbg_printf("write to the server: %s\n",buf);

		/* the last line of the header is  /r/n */
		if (!strcmp(buf,"\r\n"))
			break;
		
	}	
	
	dbg_printf("forward request to the server\n");
		
	/* receive the response from the server */
	/* initialization */
	rio_readinitb(&serverRio,con2Server);

        /* initilize for cache item */
	strcpy(recBuf,"");
        lenSum = 0;

	while(1){
		/* fill the buf with the server response stream */
		if ((length = rio_readnb(&serverRio,buf,MAXLINE))<0){
			fprintf(stderr,"fail to read from the server!\n");
			fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
			close(con);
			close(con2Server);
			pthread_exit(NULL);
		}
		
		dbg_printf("length=%d\n",length);
		dbg_printf("read response from the server: %s\n",buf);
				
		/* read from server until length =0 */
		if (length == 0)
			break;

                /* copy into temp buf   */
                if((lenSum + length)< MAX_OBJECT_SIZE){
			memcpy(&(recBuf[lenSum]),buf,length);	
		}       
                lenSum += length;

		/* send the contents of buf the client */
		if ((rio_writen(con,buf,length)) != length){
			fprintf(stderr,"fail to write to the client!\n");
			fprintf(stderr,"errno value:%d,means:%s\n",
errno,strerror(errno));
			close(con);
			close(con2Server);
			pthread_exit(NULL);
		}
		
		dbg_printf("write response to the client: %s\n",buf);
	}
	
	/* write to the cache*/
        if(lenSum < MAX_OBJECT_SIZE){
        	/* writers mutex */ 
                P(&w);
                /* do the writing*/
		if((cacheAdd(header.hostname,
header.path, recBuf,lenSum)) == 0){
			close(con);
			close(con2Server);
			pthread_exit(NULL);
		}
                V(&w);
                dbgC_printf("cache new item: %s , %s, size: %d.\n",
header.hostname, header.path,lenSum);
	}else{
                dbgC_printf("too big to cache item: %s , %s, size: %d.\n",
header.hostname, header.path,lenSum);
        }
	dbg_printf("forward response to the client\n");

	/* thread job is finished*/	
	close(con);
 	close(con2Server);
	return 0;
}

/*
 * cacheSearch: search the cache whether the request is already in the cache
 * @param (char* hostname): a pointer refering to the hostname of the request
 * @param (char* path): a pointer refering to the path of the request
 * @return (struct cacheSet*) success return pointer to the struct, failure
 * return NULL
 */
struct cacheSet *cacheSearch(char *hostname, char *path){
	struct cacheSet* tmpSet = cache->next;
        while(tmpSet != NULL){
		if((strcasecmp(tmpSet->hostname,hostname) == 0)
 && (strcasecmp(tmpSet->path,path) ==0 )){
			/* time mutex */
			P(&mutex_time);
  			tmpSet->time = globalTime;
			globalTime++;
                        V(&mutex_time);

			dbgC_printf("cache hit\n");
                        printf("cache hit\n");
			return tmpSet;
		}
		tmpSet = tmpSet->next;
	}
        dbgC_printf("cache miss\n");
	printf("cache miss\n");
        return NULL;
}

/*
 * cacheFindLRU: find the LRU set in the cache
 * @param (int len): a integer referring to the new buffer length 
 * @return (struct cache*) success return pointer, failure
 * return NULL
 */
struct cacheSet *cacheFindLRU(int len){
	int timeCount = globalTime;
        struct cacheSet* tmpSet, *tmp;
        struct cacheSet* setLRU = NULL;
	
	while((cache->len + len) > MAX_CACHE_SIZE){
		timeCount = globalTime;
		tmpSet = cache;
		setLRU = NULL;
 		/* find the LRU cache set */
		while(tmpSet->next != NULL){
			if(tmpSet->next->time <= timeCount){
				timeCount = tmpSet->next->time;
				setLRU = tmpSet;
			}
			tmpSet = tmpSet->next;
		} 
		/* update cache length */
                cache->len -= setLRU->next->len;
		if((cache->len + len) > MAX_CACHE_SIZE){
			tmp = setLRU->next;
			setLRU->next = setLRU->next->next;
			free(tmp->buffer);
			free(tmp);
		}else{
			dbgC_printf("find LRU\n");
			printf("find LRU set\n");
			return 	setLRU->next;
		}
	}       
	dbgC_printf("Error:cache could not find LRU,cache->len:%d,len:%d.\n",
 cache->len,len);
        return NULL;
}

/*
 * cacheAdd: add new set to the cache,if the cache is full,find the 
 * LRU set and free or replace it.
 * @param (char *hostname): a pointer referring to the hostname
 * @param (char *path): a pointer referring to the path
 * @param (char *buf): a pointer referrign to the buffer
 * @param (int len): a integer referring to the length of the buffer 
 * @return (int) success return 1, failure return 0 
 */
int cacheAdd(char *hostname, char *path, char *buf, int len){
	struct cacheSet* newSet;
        /* case 1 : cache still have enough space*/
	if((cache->len + len) <= MAX_CACHE_SIZE){
		newSet = malloc(sizeof(struct cacheSet));
		if(newSet == NULL){
			fprintf(stderr,"fail to malloc new cache set.\n");
			pthread_exit(NULL);
		}
		newSet->buffer = malloc(len);
		if(newSet->buffer == NULL){
			fprintf(stderr,"fail to malloc new set buffer.\n");
			pthread_exit(NULL);
		}
		memcpy(newSet->buffer, buf, len);
                newSet->len = len;
 		strcpy(newSet->hostname, hostname);
		strcpy(newSet->path, path);
		newSet->next = cache->next;
		cache->next = newSet;
              
	        dbgC_printf("cache new set\n");
		printf("cache new set\n");
	/* case 2 : cache do not have enough space, find LRU set */
	}else{
		newSet = cacheFindLRU(len);
      		if(newSet == NULL){
                	dbgC_printf("Error, newSet = NULL.\n");
			return 0;
		}
		newSet->buffer = Realloc(newSet->buffer,len);
		memcpy(newSet->buffer,buf,len);
		newSet->len = len;
                strcpy(newSet->hostname, hostname);
                strcpy(newSet->path, path);

		dbgC_printf("replace LRU set\n");
		printf("replace LRU set\n");
	}
	/* update cache length */
        cache->len += len;
	/* update global time */
	P(&mutex_time);
        newSet->time = globalTime;
	globalTime++;
        V(&mutex_time);
	dbgC_printf("cache utilization: %d\n", cache->len/MAX_CACHE_SIZE);
	printf("cache utilization: %3.2f%%\n",
((double)(100*cache->len))/MAX_CACHE_SIZE);
	return 1;
}








